<?php
/**
 * Provides function for safety storage passwords with using PBKDF2 (Password-Based Key Derivation Function) algorithm.
 */
class PBKDF2Security extends CSecurityManager
{
    /**
     * Iteration count. Higher is better, but slower. 5000 by default.
     *
     * @var int
     */
    private $iterations;

    /**
     * The length of the salt string in bytes. Default: 24
     *
     * @var
     */
    private $salt_bytes;

    /**
     * The length of the hash. Default: 24
     * @var
     */
    private $hash_bytes;

    /**
     * PBKDF2 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
     * @param $password string the password.
     * @param $salt string a salt that is unique to the password.
     * @return string generated hash.
     */
    protected function computeHMAC($password, $salt = null)
    {
        if ($salt === null)
            $salt = $this->getValidationKey();

        $hash_length = strlen(hash($this->hashAlgorithm, "", true));
        $block_count = ceil($this->hash_bytes / $hash_length);

        $output = "";
        for ($i = 1; $i <= $block_count; $i++) {
            // $i encoded as 4 bytes, big endian.
            $last = $salt . pack("N", $i);
            // first iteration
            $last = $xorsum = hash_hmac($this->hashAlgorithm, $last, $password, true);
            // perform the other $count - 1 iterations
            for ($j = 1; $j < $this->iterations; $j++) {
                $xorsum ^= ($last = hash_hmac($this->hashAlgorithm, $last, $password, true));
            }
            $output .= $xorsum;
        }

        return substr($output, 0, $this->hash_bytes);
    }

    /**
     * Creates a hash of password and generates random salt.
     *
     * @param $password string original string of password
     * @param $salt
     * @return string hash password
     */
    public function hashData($password, $salt = null)
    {
        if (is_null($salt)) {
            $salt = $this->generateRandomKey();
        }
        return base64_encode(
            $this->computeHMAC(
                $password,
                $salt
            )
        );
    }

    public function __set($name, $value)
    {
        switch ($name) {
            case "config":
                $this->hashAlgorithm = array_key_exists('algorithm', $value) ? strtolower($value['algorithm']) : 'sha256';
                $this->iterations = array_key_exists('iterations', $value) ? intval($value['iterations']) : 5000;
                $this->salt_bytes = array_key_exists('salt_bytes', $value) ? intval($value['salt_bytes']) : 24;
                $this->hash_bytes = array_key_exists('hash_bytes', $value) ? intval($value['hash_bytes']) : 24;
                if (!in_array($this->hashAlgorithm, hash_algos(), true))
                    throw new InvalidArgumentException('PBKDF2 CONFIG ERROR: Invalid hash algorithm.');
                if ($this->iterations <= 0 || $this->hash_bytes <= 0 || $this->salt_bytes <= 0)
                    throw new InvalidArgumentException('PBKDF2 CONFIG ERROR: Invalid parameters.');
                break;
            default:
                parent::__set($name, $value);
        }
    }

    protected function generateRandomKey()
    {
        return base64_encode(mcrypt_create_iv($this->salt_bytes, MCRYPT_DEV_URANDOM));
    }

    /**
     * Validates a password.
     *
     * @param $password string original password.
     * @param $hash string hash of valid password.
     * @return bool|string true if input password is valid
     */
    public function validateData($password, $hash)
    {
        return $this->slow_equals(
            $hash,
            base64_encode(
                $this->computeHMAC(
                    $password,
                    $this->getValidationKey()
                )
            )
        );
    }

    /**
     * Compares two strings $a and $b in length-constant time.
     *
     * @param $a string first string;
     * @param $b string second string.
     * @return bool if two strings are equal.
     */
    private function slow_equals($a, $b)
    {
        $diff = strlen($a) ^ strlen($b);
        $length = strlen($a);
        $i = 0;
        while ($diff === 0 && $i < $length) {
            $diff |= ord($a[$i]) ^ ord($b[$i]);
            $i++;
        }
        return $diff === 0;
    }
}
